// ArchiveExtractCallback.cpp

#include "StdAfx.h"

#include "Common/FileSystem/Wildcard.h"

#include "Common/FileSystem/FileDir.h"
#include "Common/FileSystem/FileFind.h"
#include "Common/MS/PropVariant.h"
#include "Common/String/DataConversions.h"

#include "Common/FileSystem/FilePathAutoRename.h"

#include "Common/FileSystem/ExtractingFilePath.h"

#include "ArchiveExtractCallback.h"

using namespace Common;
using namespace FileSystem;

static LPCTSTR kCantAutoRename = _T("ERROR: Can not create file with auto name");
static LPCTSTR kCantRenameFile = _T("ERROR: Can not rename existing file ");
static LPCTSTR kCantDeleteOutputFile = _T("ERROR: Can not delete output file ");

namespace Common
{
namespace Archive
{

void CArchiveExtractCallback::Init(
    const Wildcard::CCensorNode *wildcardCensor,
    const CArc *arc,
    IFolderArchiveExtractCallback *extractCallback2,
    bool stdOutMode, bool testMode, bool crcMode,
    const CString &directoryPath,
    const CStringVector &removePathParts,
    UINT64 packSize)
{
  _wildcardCensor = wildcardCensor;

  _stdOutMode = stdOutMode;
  _testMode = testMode;
  _crcMode = crcMode;
  _unpTotal = 1;
  _packTotal = packSize;

  _extractCallback2 = extractCallback2;
  _compressProgress.Release();
  _extractCallback2.QueryInterface(&_compressProgress);

  LocalProgressSpec->Init(extractCallback2, true);
  LocalProgressSpec->SendProgress = false;

 
  _removePathParts = removePathParts;
  _arc = arc;
  _directoryPath = directoryPath;
  FileSystem::FileName::NormalizeDirPathPrefix(_directoryPath);
}

STDMETHODIMP CArchiveExtractCallback::SetTotal(UINT64 size)
{
  try
  {
	_unpTotal = size;
	if (!_multiArchives && _extractCallback2)
		return _extractCallback2->SetTotal(size);
	return S_OK;
  } catch(...) { return E_OUTOFMEMORY; }
}

static void NormalizeVals(UINT64 &v1, UINT64 &v2)
{
  const UINT64 kMax = (UINT64)1 << 31;
  while (v1 > kMax)
  {
    v1 >>= 1;
    v2 >>= 1;
  }
}

static UINT64 MyMultDiv64(UINT64 unpCur, UINT64 unpTotal, UINT64 packTotal)
{
  NormalizeVals(packTotal, unpTotal);
  NormalizeVals(unpCur, unpTotal);
  if (unpTotal == 0)
    unpTotal = 1;
  return unpCur * packTotal / unpTotal;
}

STDMETHODIMP CArchiveExtractCallback::SetCompleted(const UINT64 *completeValue)
{
  try 
  {
	  if (!_extractCallback2)
		return S_OK;

	  if (_multiArchives)
	  {
		if (completeValue != NULL)
		{
		  UINT64 packCur = LocalProgressSpec->InSize + MyMultDiv64(*completeValue, _unpTotal, _packTotal);
		  return _extractCallback2->SetCompleted(&packCur);
		}
	  }
	  return _extractCallback2->SetCompleted(completeValue);
  } catch(...) 
  { return E_OUTOFMEMORY; }
}

STDMETHODIMP CArchiveExtractCallback::SetRatioInfo(const UINT64 *inSize, const UINT64 *outSize)
{
  try
  {
	return _localProgress->SetRatioInfo(inSize, outSize);
  } catch(...) 
  { return E_OUTOFMEMORY; }
}

void CArchiveExtractCallback::CreateComplexDirectory(const CStringVector &dirPathParts, CString &fullPath)
{
  fullPath = _directoryPath;
  for (size_t i = 0; i < dirPathParts.size(); i++)
  {
    if (i > 0)
      fullPath += wchar_t(FileName::kDirDelimiter);
    fullPath += dirPathParts[i];
    Common::FileSystem::CreateDirectory(fullPath);
  }
}

HRESULT CArchiveExtractCallback::GetTime(int index, PROPID propID, FILETIME &filetime, bool &filetimeIsDefined)
{
  filetimeIsDefined = false;
  CComPropVariant prop;
  HRESULT hRes = _arc->Archive->GetProperty(index, propID, &prop);
  if(S_OK != hRes)
		return hRes;
  if (prop.vt == VT_FILETIME)
  {
    filetime = prop.filetime;
    filetimeIsDefined = (filetime.dwHighDateTime != 0 || filetime.dwLowDateTime != 0);
  }
  else if (prop.vt != VT_EMPTY)
    return E_FAIL;
  return S_OK;
}

HRESULT CArchiveExtractCallback::GetUnpackSize()
{
  CComPropVariant prop;
  HRESULT hRes(_arc->Archive->GetProperty(_index, kpidSize, &prop));
  if(S_OK != hRes)
		return hRes;
  _curSizeDefined = (prop.vt != VT_EMPTY);
  if (_curSizeDefined)
    _curSize = String::ConvertPropVariantToUInt64(prop);
  return S_OK;
}

//helper functions extracted from GetStream

HRESULT CArchiveExtractCallback::GetFileProperties(int index)
{
	_encrypted = false;
	_isSplit = false;
	_curSize = 0;
	_curSizeDefined = false;
	_index = index;

	CString fullPath;
	  	  
	HRESULT hRes(S_OK);

	hRes = _arc->GetItemPath(index, fullPath);
	if(S_OK != hRes)
	return hRes;
	  
	IInArchive *archive = _arc->Archive;

	hRes = IsArchiveItemFolder(archive, index, _fi.IsDir);
	if(S_OK != hRes)
	return hRes;

	_filePath = fullPath;

	{
	CComPropVariant prop;
	hRes = archive->GetProperty(index, kpidPosition, &prop);
	if(S_OK != hRes)
		return hRes;
	if (prop.vt != VT_EMPTY)
	{
		if (prop.vt != VT_UI8)
		return E_FAIL;
		_position = prop.uhVal.QuadPart;
		_isSplit = true;
	}
	}
    
	hRes = GetArchiveItemBoolProp(archive, index, kpidEncrypted, _encrypted);
	if(S_OK != hRes)
	return hRes;

	hRes = GetUnpackSize();
	if(S_OK != hRes)
	return hRes;
	return hRes;
}

HRESULT CArchiveExtractCallback::GetArcFileProperties(int index)
{
	HRESULT hRes(S_OK);
	{
		  CComPropVariant prop;
		  IInArchive *archive = _arc->Archive;
		  hRes = archive->GetProperty(index, kpidAttrib, &prop);
		  if(S_OK != hRes)
			return hRes;
		  if (prop.vt == VT_UI4)
		  {
			_fi.Attrib = prop.ulVal;
			_fi.AttribDefined = true;
		  }
		  else if (prop.vt == VT_EMPTY)
			_fi.AttribDefined = false;
		  else
			return E_FAIL;
		}

		hRes = GetTime(index, kpidCTime, _fi.CTime, _fi.CTimeDefined);
		if(S_OK != hRes)
			return hRes;
		hRes = GetTime(index, kpidATime, _fi.ATime, _fi.ATimeDefined);
		if(S_OK != hRes)
			return hRes;
		hRes = GetTime(index, kpidMTime, _fi.MTime, _fi.MTimeDefined);
		if(S_OK != hRes)
			return hRes;
	return hRes;
}


STDMETHODIMP CArchiveExtractCallback::GetStream(UINT32 index, ISequentialOutStream **outStream, INT32 askExtractMode)
{
  try
  {
	  _crcStream.Release();
	  *outStream = 0;
	  _outFileStream.Release();

	  HRESULT hRes = GetFileProperties(index);
	  CString fullPath = _filePath;
	  if(FAILED(hRes))
		  return hRes;

	  if (_wildcardCensor)
	  {
		if (!_wildcardCensor->CheckPath(fullPath, !_fi.IsDir))
		  return S_OK;
	  }

	  if (static_cast<INT32>(Archive::Extract::AskMode::kExtract) == askExtractMode && !_testMode)
	  {
		if (_stdOutMode)
		{
		  CStdOutFileStream * stream = new CComObject<CStdOutFileStream>;
		  stream->FinalConstruct();
		  CComPtr<ISequentialOutStream> outStreamLoc = stream;
		  
		  *outStream = outStreamLoc.Detach();
		  return S_OK;
		}

		GetArcFileProperties(index);
			
		bool isAnti = false;
		hRes = _arc->IsItemAnti(index, isAnti);
		if(S_OK != hRes)
			return hRes;

		CStringVector pathParts;
		SplitPathToParts(fullPath, pathParts);
    
		if (pathParts.empty())
		  return E_FAIL;
		size_t numRemovePathParts = 0;
		switch(_pathMode)
		{
		  case Archive::Extract::PathMode::kFullPathnames:
			break;
		  case Archive::Extract::PathMode::kCurrentPathnames:
		  {
			numRemovePathParts = _removePathParts.size();
			if (pathParts.size() <= numRemovePathParts)
			  return E_FAIL;
			for (size_t i = 0; i < numRemovePathParts; i++)
			  if (_removePathParts[i].CompareNoCase(pathParts[i]) != 0)
				return E_FAIL;
			break;
		  }
		  case Archive::Extract::PathMode::kNoPathnames:
		  {
			numRemovePathParts = pathParts.size() - 1;
			break;
		  }
		}
		pathParts.erase(pathParts.begin(), pathParts.begin() + numRemovePathParts);
		MakeCorrectPath(pathParts);
		CString processedPath = MakePathNameFromParts(pathParts);
		if (!isAnti)
		{
		  if (!_fi.IsDir)
		  {
			if (!pathParts.empty())
				pathParts.pop_back();
		  }
    
		  if (!pathParts.empty())
		  {
			CString fullPathNew;
			CreateComplexDirectory(pathParts, fullPathNew);
			if (_fi.IsDir)
			  FileSystem::SetDirTime(fullPathNew,
				(WriteCTime && _fi.CTimeDefined) ? &_fi.CTime : NULL,
				(WriteATime && _fi.ATimeDefined) ? &_fi.ATime : NULL,
				(WriteMTime && _fi.MTimeDefined) ? &_fi.MTime : (_arc->MTimeDefined ? &_arc->MTime : NULL));
		  }
		}


		CString fullProcessedPath = _directoryPath + processedPath;

		if (_fi.IsDir)
		{
		  _diskFilePath = fullProcessedPath;
		  if (isAnti)
			Common::FileSystem::RemoveDirectory(_diskFilePath);
		  return S_OK;
		}

		if (!_isSplit)
		{
			FileSystem::File::CFileInfo fileInfo;
			if (fileInfo.Find(fullProcessedPath))
			{
			  switch(_overwriteMode)
			  {
				case Archive::Extract::OverwriteMode::kSkipExisting:
				  return S_OK;
				case Archive::Extract::OverwriteMode::kAskBefore:
				{
				  Archive::OverwriteAnswer overwriteResult;
				  hRes = _extractCallback2->AskOverwrite(
					  fullProcessedPath, &fileInfo.ModificationTime, &fileInfo.Size, fullPath,
					  _fi.MTimeDefined ? &_fi.MTime : NULL,
					  _curSizeDefined ? &_curSize : NULL,
					  &overwriteResult);
				  if(S_OK != hRes)
					return hRes;

				  switch(overwriteResult)
				  {
					case Archive::OverwriteAnswer::kCancel:
					  return E_ABORT;
					case Archive::OverwriteAnswer::kNo:
					  return S_OK;
					case Archive::OverwriteAnswer::kNoToAll:
					  _overwriteMode = Archive::Extract::OverwriteMode::kSkipExisting;
					  return S_OK;
					case Archive::OverwriteAnswer::kYesToAll:
					  _overwriteMode = Archive::Extract::OverwriteMode::kWithoutPrompt;
					  break;
					case Archive::OverwriteAnswer::kYes:
					  break;					
					default:
					  return E_FAIL;
				  }
				}
			  }
			  if (Archive::Extract::OverwriteMode::kAutoRename == _overwriteMode)
			  {
				if (!AutoRenamePath(fullProcessedPath))
				{
				  CString message = CString(kCantAutoRename) + fullProcessedPath;
				  hRes = _extractCallback2->MessageError(message);		  
				  return E_FAIL;
				}
			  }
			  else if (Archive::Extract::OverwriteMode::kAutoRenameExisting == _overwriteMode)
			  {
				CString existPath = fullProcessedPath;
				if (!AutoRenamePath(existPath))
				{
				  CString message = kCantAutoRename + fullProcessedPath;
				  hRes = _extractCallback2->MessageError(message);
				  return E_FAIL;
				}
				if (!Common::FileSystem::MoveFile(fullProcessedPath, existPath))
				{
				  CString message = CString(kCantRenameFile) + fullProcessedPath;
				  hRes = _extractCallback2->MessageError(message);
				  return E_FAIL;
				}
			  }
			  else
				if (!Common::FileSystem::DeleteFileAlways(fullProcessedPath))
				{
				  CString message = CString(kCantDeleteOutputFile) +  fullProcessedPath;
				  hRes = _extractCallback2->MessageError(message);
				  return S_OK;
				  // return E_FAIL;
				}
			}
		}
		if (!isAnti)
		{
		  _outFileStreamSpec = new CComObject<COutFileStream>;
		  _outFileStreamSpec->FinalConstruct();
		  CComPtr<ISequentialOutStream> outStreamLoc(_outFileStreamSpec);
		  if (!_outFileStreamSpec->Open(fullProcessedPath, _isSplit ? OPEN_ALWAYS: CREATE_ALWAYS))
		  {
			// if (::GetLastError() != ERROR_FILE_EXISTS || !isSplit)
			{
			  CString message = _T("can not open output file ") + fullProcessedPath;
			  hRes = _extractCallback2->MessageError(message);
			  return S_OK;
			}
		  }
		  if (_isSplit)
		  {
			hRes = _outFileStreamSpec->Seek(_position, STREAM_SEEK_SET, NULL);
			if(S_OK != hRes)
				return hRes;
		  }
		  _outFileStream = outStreamLoc;
		  *outStream = outStreamLoc.Detach();
		}
		_diskFilePath = fullProcessedPath;
	  }
	  else
	  {
		*outStream = NULL;
	  }
	  if (_crcMode)
	  {
		_crcStreamSpec = new CComObject<COutStreamWithCRC>;
		_crcStreamSpec->FinalConstruct();
		_crcStream = _crcStreamSpec;
		CComPtr<ISequentialOutStream> crcStream = _crcStreamSpec;
		_crcStreamSpec->SetStream(*outStream);
		if (*outStream)
		  (*outStream)->Release();
		*outStream = crcStream.Detach();
		_crcStreamSpec->Init(true);
	  }
	  return S_OK;
  } catch(...) 
  { return E_OUTOFMEMORY; }
}

STDMETHODIMP CArchiveExtractCallback::PrepareOperation(INT32 askExtractMode)
{
  try
  {
	  _extractMode = false;
	  switch (askExtractMode)
	  {
		case Archive::Extract::AskMode::kExtract:
		  if (_testMode)
			askExtractMode = (static_cast<INT32>(Archive::Extract::AskMode::kTest));
		  else
			_extractMode = true;
		  break;
	  };
	  return _extractCallback2->PrepareOperation(_filePath, _fi.IsDir,
		  askExtractMode, _isSplit ? &_position: 0);
  } catch(...) 
  { return E_OUTOFMEMORY; }
}

STDMETHODIMP CArchiveExtractCallback::SetOperationResult(INT32 operationResult)
{
  try
  {
	  switch(operationResult)
	  {
		case Extract::OperationResult::kOK:
		case Extract::OperationResult::kUnSupportedMethod:
		case Extract::OperationResult::kCRCError:
		case Extract::OperationResult::kDataError:
		  break;
		default:
		  _outFileStream.Release();
		  return E_FAIL;
	  }
	  if (_crcStream)
	  {
		CrcSum += _crcStreamSpec->GetCRC();
		_curSize = _crcStreamSpec->GetSize();
		_curSizeDefined = true;
		_crcStream.Release();
	  }
	  HRESULT hRes(S_OK);
	  if (_outFileStream)
	  {
		_outFileStreamSpec->SetTime(
			(WriteCTime && _fi.CTimeDefined) ? &_fi.CTime : NULL,
			(WriteATime && _fi.ATimeDefined) ? &_fi.ATime : NULL,
			(WriteMTime && _fi.MTimeDefined) ? &_fi.MTime : (_arc->MTimeDefined ? &_arc->MTime : NULL));
		_curSize = _outFileStreamSpec->ProcessedSize;
		_curSizeDefined = true;
		hRes = _outFileStreamSpec->Close();
		if(S_OK != hRes)
			return hRes;
		_outFileStream.Release();
	  }
	  if (!_curSizeDefined)
		GetUnpackSize();
	  if (_curSizeDefined)
		UnpackSize += _curSize;
	  if (_fi.IsDir)
		NumFolders++;
	  else
		NumFiles++;

	  if (_extractMode && _fi.AttribDefined)
		Common::FileSystem::SetFileAttributes(_diskFilePath, _fi.Attrib);
	  hRes = _extractCallback2->SetOperationResult(operationResult, _encrypted);
	  if(S_OK != hRes)
		return hRes;
	  return S_OK;
  } catch(...) 
  { return E_OUTOFMEMORY; }
}

/*
STDMETHODIMP CArchiveExtractCallback::GetInStream(
    const wchar_t *name, ISequentialInStream **inStream)
{
  try {
  CInFileStream *inFile = new CInFileStream;
  CComPtr<ISequentialInStream> inStreamTemp = inFile;
  if (!inFile->Open(_srcDirectoryPrefix + name))
    return ::GetLastError();
  *inStream = inStreamTemp.Detach();
  return S_OK;
  } catch(...) 
  { return E_OUTOFMEMORY; }
}
*/

STDMETHODIMP CArchiveExtractCallback::CryptoGetTextPassword(BSTR *password)
{
  try
  {
	  if (!_cryptoGetTextPassword)
	  {
		HRESULT hRes = _extractCallback2.QueryInterface(&_cryptoGetTextPassword);
		if(S_OK != hRes)
			return hRes;
	  }
	  return _cryptoGetTextPassword->CryptoGetTextPassword(password);
  } catch(...) 
  { return E_OUTOFMEMORY; }
}

}
}
